53. Pandas模块的统计计算

⭐ 本章学习目标

  • 掌握 Pandas 描述性统计方法(均值、中位数、标准差等)
  • 理解偏度与峰度的金融含义
  • 学会使用 累积统计量 进行回撤分析
  • 掌握 滚动窗口(rolling)与扩展窗口(expanding)计算
  • 运用 groupby 进行分组统计
  • 理解协方差与相关系数在投资组合中的作用

⭐ 统计计算是数据分析的核心

统计学是从数据中提取知识的科学。在金融领域,统计计算帮助我们:

  • 量化风险:计算波动率、VaR(在险价值)
  • 评估收益:计算收益率、夏普比率
  • 发现规律:识别相关性、趋势、周期性
  • 做出决策:基于统计显著性的投资决策

Pandas 提供了丰富的统计计算方法,结合 NumPy 的向量化运算,可以高效处理大规模金融数据。

⭐ 描述性统计:集中趋势

给定数据集 \(X = \{x_1, x_2, \ldots, x_n\}\)

统计量 含义 公式
均值 (Mean) 数据的”重心” \(\bar{x} = \frac{1}{n}\sum_{i=1}^n x_i\)
中位数 (Median) 排序后中间位置的值 将数据分为两半
众数 (Mode) 出现频率最高的值 最常见的观测值

⭐ 描述性统计:离散程度与分布形状

统计量 含义 公式
方差 (Variance) 偏离均值的平均平方距离 \(\sigma^2 = \frac{1}{n-1}\sum_{i=1}^n (x_i - \bar{x})^2\)
标准差 (Std Dev) 方差的平方根 \(\sigma = \sqrt{\sigma^2}\)
极差 (Range) 最大值与最小值之差 \(\max(X) - \min(X)\)

分布形状

  • 偏度 (Skewness):衡量分布的对称性
  • 峰度 (Kurtosis):衡量分布的尖峰/厚尾程度

⭐ 平台任务解答代码

# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
#任务一
import pandas as pd
import matplotlib.pyplot as plt  # 导入Matplotlib绑图库
# 从Excel文件读取数据存入value_QDII
value_QDII = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/1726648479386.xlsx") 
value_QDII["日期"] = pd.to_datetime(value_QDII["日期"] , format='%Y%m%d')  # 转换为日期时间格式
value_QDII.set_index("日期",inplace=True)  # 将日期列设为value_QDII数据框的索引
value_QDII = value_QDII.dropna() #删除缺失值所在行
(value_QDII/value_QDII.iloc[0]).plot(figsize=(8,6),grid=True) #将基金净值按首个交易日进行归一处理并可视化
plt.savefig("1.png")  # 保存图形至文件

#任务二
import pandas as pd
# 从Excel文件读取数据存入value_QDII
value_QDII = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/1726648479386.xlsx") 
value_QDII["日期"] = pd.to_datetime(value_QDII["日期"] , format='%Y%m%d')  # 转换为日期时间格式
value_QDII.set_index("日期",inplace=True)  # 将日期列设为value_QDII数据框的索引
print(value_QDII.max())  #找出每只基金净值的最大值
print(value_QDII.min())  #找出每只基金净值的最小值
print(value_QDII.idxmax()) #最大值所在的索引值
print(value_QDII.idxmin()) #最小值所在的索引值


#任务三
import pandas as pd
# 从Excel文件读取数据存入value_QDII
value_QDII = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/1726648479386.xlsx") 
value_QDII["日期"] = pd.to_datetime(value_QDII["日期"] , format='%Y%m%d')  # 转换为日期时间格式
value_QDII.set_index("日期",inplace=True)  # 将日期列设为value_QDII数据框的索引
value_QDII_diff = value_QDII.diff()  # 计算基金每日净值的变动金额
print(value_QDII_diff.head())  #查看前五行数据
print(value_QDII_diff.tail())  #查看后五行数据

#任务四
import pandas as pd
# 从Excel文件读取数据存入value_QDII
value_QDII = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/1726648479386.xlsx") 
value_QDII_pctchangel = value_QDII.pct_change() #直接使用函数pct_change计算基金每日净值百分比变动

value_QDII_pctchangel.head()  # 查看value_QDII_pctchangel前5行数据
value_QDII_pctchangel.tail()  # 查看value_QDII_pctchangel后5行数据
value_QDII_diff = value_QDII.diff()  # 计算差分值
value_QDII_pctchange2 = value_QDII_diff/value_QDII.shift(1) #运用任务三的结果计算基金每日净值百分比变动
print(value_QDII_pctchange2.head())  # 输出前几行数据
print(value_QDII_pctchange2.tail())  # 输出最后几行数据
华夏全球股票       1.1114
华安香港精选股票     1.8300
工银瑞信全球股票     2.3920
易方达亚洲精选股票    1.0890
dtype: float64
华夏全球股票       0.9131
华安香港精选股票     1.4310
工银瑞信全球股票     2.1320
易方达亚洲精选股票    0.8460
dtype: float64
华夏全球股票      2024-06-19
华安香港精选股票    2024-06-20
工银瑞信全球股票    2024-05-20
易方达亚洲精选股票   2024-06-19
dtype: datetime64[ns]
华夏全球股票      2024-01-04
华安香港精选股票    2024-01-22
工银瑞信全球股票    2024-01-17
易方达亚洲精选股票   2024-01-17
dtype: datetime64[ns]
            华夏全球股票  华安香港精选股票  工银瑞信全球股票  易方达亚洲精选股票
日期                                               
2024-01-04     NaN       NaN       NaN        NaN
2024-01-05  0.0004    -0.011    -0.004     -0.006
2024-01-08  0.0083    -0.023    -0.001     -0.002
2024-01-09  0.0024     0.006    -0.002     -0.009
2024-01-10  0.0045    -0.001     0.004      0.000
            华夏全球股票  华安香港精选股票  工银瑞信全球股票  易方达亚洲精选股票
日期                                               
2024-06-25  0.0109    -0.001     0.006      0.009
2024-06-26  0.0016     0.004     0.003      0.001
2024-06-27 -0.0019    -0.024    -0.012     -0.009
2024-06-28 -0.0056     0.033    -0.008     -0.001
2024-06-30 -0.0001     0.000     0.000     -0.001
             日期    华夏全球股票  华安香港精选股票  工银瑞信全球股票  易方达亚洲精选股票
0           NaN       NaN       NaN       NaN        NaN
1  4.940686e-08  0.000438 -0.007079 -0.001860  -0.006842
2  1.482206e-07  0.009086 -0.014906 -0.000466  -0.002296
3  4.940685e-08  0.002604  0.003947 -0.000932  -0.010357
4  4.940685e-08  0.004869 -0.000655  0.001866   0.000000
               日期    华夏全球股票  华安香港精选股票  工银瑞信全球股票  易方达亚洲精选股票
112  4.940559e-08  0.009960 -0.000560  0.002565   0.008499
113  4.940559e-08  0.001448  0.002242  0.001279   0.000936
114  4.940559e-08 -0.001717 -0.013423 -0.005111  -0.008419
115  4.940558e-08 -0.005068  0.018707 -0.003425  -0.000943
116  9.881116e-08 -0.000091  0.000000  0.000000  -0.000944
Listing 1

⭐ 基础统计量计算

Listing 2
import pandas as pd
import numpy as np

# 创建股票收益率数据
np.random.seed(42)
returns_data = {
    '贵州茅台': np.random.normal(0.001, 0.02, 100),
    '五粮液': np.random.normal(0.0008, 0.025, 100),
    '招商银行': np.random.normal(0.0005, 0.015, 100),
    '中国平安': np.random.normal(0.0006, 0.018, 100)
}

df_returns = pd.DataFrame(returns_data)

print('收益率数据(前5行):')
print(df_returns.head())
收益率数据(前5行):
       贵州茅台       五粮液      招商银行      中国平安
0  0.010934 -0.034584  0.005867 -0.014322
1 -0.001765 -0.009716  0.008912 -0.009483
2  0.013954 -0.007768  0.016746  0.014051
3  0.031461 -0.019257  0.016307  0.011587
4 -0.003683 -0.003232 -0.020165  0.000224

⭐ 常用统计方法一览

Listing 3
# 计算各项统计量
print('均值:')
print(df_returns.mean())
print('\n标准差:')
print(df_returns.std())
print('\n最小值:')
print(df_returns.min())
print('\n最大值:')
print(df_returns.max())
均值:
贵州茅台   -0.001077
五粮液     0.001358
招商银行    0.001473
中国平安    0.002523
dtype: float64

标准差:
贵州茅台    0.018163
五粮液     0.023842
招商银行    0.016264
中国平安    0.015914
dtype: float64

最小值:
贵州茅台   -0.051395
五粮液    -0.047169
招商银行   -0.048119
中国平安   -0.037630
dtype: float64

最大值:
贵州茅台    0.038046
五粮液     0.068804
招商银行    0.058291
中国平安    0.040016
dtype: float64

⭐ describe() 一键获取完整统计

Listing 4
desc_stats = df_returns.describe()
print(desc_stats)
             贵州茅台         五粮液        招商银行        中国平安
count  100.000000  100.000000  100.000000  100.000000
mean    -0.001077    0.001358    0.001473    0.002523
std      0.018163    0.023842    0.016264    0.015914
min     -0.051395   -0.047169   -0.048119   -0.037630
25%     -0.011018   -0.019342   -0.009332   -0.009606
50%     -0.001539    0.002903    0.001965    0.001503
75%      0.009119    0.014254    0.011067    0.012912
max      0.038046    0.068804    0.058291    0.040016

⭐ describe() 返回统计量详解

统计量 含义 公式
count 非缺失值数量 \(n_{\text{valid}}\)
mean 均值 \(\bar{x} = \frac{1}{n}\sum x_i\)
std 标准差 \(\sqrt{\frac{1}{n-1}\sum(x_i-\bar{x})^2}\)
min 最小值 \(\min(X)\)
25% 第一四分位数 \(Q_1 = P_{25}\)
50% 中位数 \(Q_2 = P_{50}\)
75% 第三四分位数 \(Q_3 = P_{75}\)
max 最大值 \(\max(X)\)

⭐ 分位数与自定义分位数计算

Listing 5
# 计算常用分位数
quantiles = [0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99]
print('自定义分位数:')
print(df_returns.quantile(quantiles))
自定义分位数:
          贵州茅台       五粮液      招商银行      中国平安
0.10 -0.025732 -0.029081 -0.018236 -0.014619
0.25 -0.011018 -0.019342 -0.009332 -0.009606
0.50 -0.001539  0.002903  0.001965  0.001503
0.75  0.009119  0.014254  0.011067  0.012912
0.90  0.021126  0.030136  0.018103  0.023648
0.95  0.030603  0.047470  0.027249  0.028458
0.99  0.032639  0.062445  0.035451  0.039364

⭐ 四分位距与异常值检测

Listing 6
# 四分位距(IQR)
Q1 = df_returns.quantile(0.25)
Q3 = df_returns.quantile(0.75)
IQR = Q3 - Q1
print('四分位距(IQR):')
print(IQR)

# 识别异常值(超出1.5*IQR)
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

# 检测异常值
outliers = (df_returns < lower_bound) | (df_returns > upper_bound)
print(f'\n异常值数量:')
print(outliers.sum())
四分位距(IQR):
贵州茅台    0.020137
五粮液     0.033596
招商银行    0.020398
中国平安    0.022518
dtype: float64

异常值数量:
贵州茅台    1
五粮液     1
招商银行    2
中国平安    0
dtype: int64

⭐ 异常值检测的数学原理

箱线图规则 (Boxplot Rule):

  • 正常值\([Q_1 - 1.5 \times IQR, \; Q_3 + 1.5 \times IQR]\)
  • 温和异常值:距离箱体 \(1.5 \sim 3 \times IQR\)
  • 极端异常值:距离箱体 \(> 3 \times IQR\)

Z-Score 方法

\[ Z_i = \frac{x_i - \bar{x}}{\sigma} \]

通常认为 \(|Z| > 3\) 为异常值。

⭐ 偏度:分布的对称性

偏度 (Skewness) 的数学定义:

\[ \gamma_1 = \frac{E[(X-\mu)^3]}{\sigma^3} = \frac{\frac{1}{n}\sum_{i=1}^n (x_i-\bar{x})^3}{\left[\sqrt{\frac{1}{n}\sum_{i=1}^n (x_i-\bar{x})^2}\right]^3} \]

  • \(\gamma_1 > 0\):右偏(正偏),右尾较长
  • \(\gamma_1 = 0\):对称分布
  • \(\gamma_1 < 0\):左偏(负偏),左尾较长

⭐ 峰度:分布的厚尾程度

峰度 (Kurtosis) 的数学定义(超额峰度):

\[ \gamma_2 = \frac{E[(X-\mu)^4]}{\sigma^4} - 3 \]

  • \(\gamma_2 > 0\):尖峰(厚尾),极端事件概率高于正态分布
  • \(\gamma_2 = 0\):正态分布
  • \(\gamma_2 < 0\):低峰(薄尾),较为平坦

正态分布检验:若数据完全服从正态分布,则 \(\gamma_1 \approx 0\)\(\gamma_2 \approx 0\)

⭐ 偏度与峰度的金融意义

指标 特征 金融含义
正偏度 右尾较长 偶发大幅上涨(牛市特征),符合投资者偏好
负偏度 左尾较长 偶发暴跌(黑天鹅事件),风险较高
高峰度 厚尾分布 极端事件概率高,VaR 模型需特别注意

金融市场收益率通常表现为负偏度 + 高峰度,即”尖峰厚尾”特征。

⭐ 计算偏度和峰度

Listing 7
skewness = df_returns.skew()
kurtosis = df_returns.kurtosis()

print('偏度(Skewness):')
print(skewness)
print('\n峰度(Kurtosis):')
print(kurtosis)

# 解释
for stock in df_returns.columns:
    skew_val = skewness[stock]
    kurt_val = kurtosis[stock]
    if skew_val > 0.5:
        skew_interp = '右偏,极端正收益更多'
    elif skew_val < -0.5:
        skew_interp = '左偏,极端负收益更多'
    else:
        skew_interp = '近似对称'
    if kurt_val > 1:
        kurt_interp = '尖峰厚尾'
    elif kurt_val < -1:
        kurt_interp = '低峰薄尾'
    else:
        kurt_interp = '接近正态'
    print(f'{stock}: 偏度={skew_val:.3f}({skew_interp}), 峰度={kurt_val:.3f}({kurt_interp})')
偏度(Skewness):
贵州茅台   -0.177948
五粮液     0.386984
招商银行    0.177762
中国平安    0.199641
dtype: float64

峰度(Kurtosis):
贵州茅台   -0.100977
五粮液     0.030979
招商银行    1.125855
中国平安   -0.184679
dtype: float64
贵州茅台: 偏度=-0.178(近似对称), 峰度=-0.101(接近正态)
五粮液: 偏度=0.387(近似对称), 峰度=0.031(接近正态)
招商银行: 偏度=0.178(近似对称), 峰度=1.126(尖峰厚尾)
中国平安: 偏度=0.200(近似对称), 峰度=-0.185(接近正态)

⭐ 累积统计量:cumsum 与 cumprod

Listing 8
prices = pd.DataFrame({
    '贵州茅台': [1850, 1860, 1855, 1870, 1865, 1880],
    '五粮液': [220, 218, 222, 225, 223, 226]
})

print('原始价格:')
print(prices)
print('\n累积和(cumsum):')
print(prices.cumsum())
原始价格:
   贵州茅台  五粮液
0  1850  220
1  1860  218
2  1855  222
3  1870  225
4  1865  223
5  1880  226

累积和(cumsum):
    贵州茅台   五粮液
0   1850   220
1   3710   438
2   5565   660
3   7435   885
4   9300  1108
5  11180  1334

⭐ 累积最大值与累积最小值

Listing 9
print('累积最大值(cummax):')
print(prices.cummax())
print('\n累积最小值(cummin):')
print(prices.cummin())

# 累计收益率
initial_prices = prices.iloc[0]
cum_returns = (prices / initial_prices - 1) * 100
print('\n累计收益率(%):')
print(cum_returns)
累积最大值(cummax):
   贵州茅台  五粮液
0  1850  220
1  1860  220
2  1860  222
3  1870  225
4  1870  225
5  1880  226

累积最小值(cummin):
   贵州茅台  五粮液
0  1850  220
1  1850  218
2  1850  218
3  1850  218
4  1850  218
5  1850  218

累计收益率(%):
       贵州茅台       五粮液
0  0.000000  0.000000
1  0.540541 -0.909091
2  0.270270  0.909091
3  1.081081  2.272727
4  0.810811  1.363636
5  1.621622  2.727273

⭐ 累积统计量的金融应用

方法 金融应用场景
cumsum 累计收益、累计成交量
cumprod 复利增长(价格相对变化)
cummax 回撤分析(Drawdown)
cummin 历史最低价监控

⭐ 最大回撤:衡量投资风险的关键指标

回撤 (Drawdown):从历史最高点到当前点的下降幅度

\[ \text{Drawdown}_t = \frac{P_t - \max_{i \leq t} P_i}{\max_{i \leq t} P_i} \]

最大回撤 (Maximum Drawdown, MDD):

\[ \text{MDD} = \min_t \text{Drawdown}_t \]

  • MDD = -20% 意味着曾从高点下跌 20%
  • 下跌 20% 需要上涨 25% 才能回本

⭐ 最大回撤的代码实现

Listing 10
np.random.seed(42)
nav = pd.DataFrame({
    '日期': pd.date_range('2024-01-01', periods=100),
    '净值': 1.0 + np.cumsum(np.random.normal(0.001, 0.02, 100))
})

# 计算历史最高点与回撤
nav['历史最高'] = nav['净值'].cummax()
nav['回撤'] = (nav['净值'] - nav['历史最高']) / nav['历史最高']

max_drawdown = nav['回撤'].min()
print(f'最大回撤: {max_drawdown:.2%}')
print(nav[['日期', '净值', '历史最高', '回撤']].head(15))
最大回撤: -26.09%
           日期        净值      历史最高        回撤
0  2024-01-01  1.010934  1.010934  0.000000
1  2024-01-02  1.009169  1.010934 -0.001746
2  2024-01-03  1.023123  1.023123  0.000000
3  2024-01-04  1.054583  1.054583  0.000000
4  2024-01-05  1.050900  1.054583 -0.003492
5  2024-01-06  1.047218  1.054583 -0.006985
6  2024-01-07  1.079802  1.079802  0.000000
7  2024-01-08  1.096151  1.096151  0.000000
8  2024-01-09  1.087761  1.096151 -0.007654
9  2024-01-10  1.099612  1.099612  0.000000
10 2024-01-11  1.091344  1.099612 -0.007519
11 2024-01-12  1.083029  1.099612 -0.015081
12 2024-01-13  1.088869  1.099612 -0.009770
13 2024-01-14  1.051603  1.099612 -0.043660
14 2024-01-15  1.018105  1.099612 -0.074124

⭐ 净值与回撤可视化

import matplotlib.pyplot as plt

plt.rcParams['font.sans-serif'] = ['Source Han Serif SC', 'SimHei']
plt.rcParams['axes.unicode_minus'] = False

fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8), sharex=True)

ax1.plot(nav['日期'], nav['净值'], label='净值', linewidth=2)
ax1.plot(nav['日期'], nav['历史最高'], label='历史最高', linewidth=2, linestyle='--')
ax1.set_ylabel('净值')
ax1.set_title('净值曲线与回撤分析')
ax1.legend()
ax1.grid(True, alpha=0.3)

ax2.fill_between(nav['日期'], nav['回撤'], 0, alpha=0.3, color='red')
ax2.plot(nav['日期'], nav['回撤'], color='red', linewidth=2)
ax2.set_ylabel('回撤率')
ax2.set_xlabel('日期')
ax2.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()
Figure 1: 净值曲线与回撤分析

⭐ 滚动窗口统计:数学原理

滚动窗口 (Rolling Window):对时间序列 \(\{x_t\}_{t=1}^T\),窗口大小为 \(w\)

\[ \text{RollingMean}_t = \frac{1}{w}\sum_{i=t-w+1}^t x_i \]

核心思想:只用最近 \(w\) 个数据点计算统计量,捕捉短期动态变化

⭐ 滚动窗口计算实例

Listing 11
dates = pd.date_range('2024-01-01', periods=100)
prices_ts = pd.DataFrame({
    '日期': dates,
    '收盘价': 100 + np.cumsum(np.random.normal(0.5, 2, 100))
})
prices_ts = prices_ts.set_index('日期')

# 计算滚动统计量
prices_ts['MA5'] = prices_ts['收盘价'].rolling(window=5).mean()
prices_ts['MA20'] = prices_ts['收盘价'].rolling(window=20).mean()
prices_ts['STD5'] = prices_ts['收盘价'].rolling(window=5).std()

print('滚动统计量(最后10行):')
print(prices_ts.tail(10))
滚动统计量(最后10行):
                   收盘价         MA5        MA20      STD5
日期                                                      
2024-03-31  152.824201  155.063008  149.335599  1.919111
2024-04-01  155.036999  154.753174  150.176524  1.727180
2024-04-02  155.965186  154.558108  151.046569  1.451454
2024-04-03  153.973709  154.203465  151.757924  1.296751
2024-04-04  154.820070  154.524033  152.458929  1.185325
2024-04-05  156.090705  155.177334  153.115747  0.873356
2024-04-06  154.822990  155.134532  153.682879  0.887089
2024-04-07  155.630441  155.067583  154.120030  0.818724
2024-04-08  156.246858  155.522213  154.589468  0.678587
2024-04-09  154.460917  155.450382  154.672592  0.782539

⭐ 布林带:基于滚动窗口的技术指标

布林带 (Bollinger Bands) 由三条线组成:

  • 中轨\(MA_{20}\)(20日移动平均)
  • 上轨\(MA_{20} + 2\sigma\)
  • 下轨\(MA_{20} - 2\sigma\)

交易信号:价格触及上轨可能超买,触及下轨可能超卖。

Listing 12
prices_ts['布林带_上'] = prices_ts['MA20'] + 2 * prices_ts['收盘价'].rolling(window=20).std()
prices_ts['布林带_下'] = prices_ts['MA20'] - 2 * prices_ts['收盘价'].rolling(window=20).std()

print('布林带(最后10行):')
print(prices_ts[['收盘价', 'MA20', '布林带_上', '布林带_下']].tail(10))
布林带(最后10行):
                   收盘价        MA20       布林带_上       布林带_下
日期                                                        
2024-03-31  152.824201  149.335599  162.267536  136.403661
2024-04-01  155.036999  150.176524  162.221520  138.131528
2024-04-02  155.965186  151.046569  162.026592  140.066545
2024-04-03  153.973709  151.757924  161.419814  142.096034
2024-04-04  154.820070  152.458929  160.703794  144.214064
2024-04-05  156.090705  153.115747  160.181141  146.050353
2024-04-06  154.822990  153.682879  159.126562  148.239196
2024-04-07  155.630441  154.120030  158.581624  149.658436
2024-04-08  156.246858  154.589468  157.560664  151.618272
2024-04-09  154.460917  154.672592  157.523393  151.821791

⭐ expanding 窗口:累积统计

Listing 13
prices_ts['累积均值'] = prices_ts['收盘价'].expanding().mean()
prices_ts['累积标准差'] = prices_ts['收盘价'].expanding().std()
prices_ts['累积最大值'] = prices_ts['收盘价'].expanding().max()

print('扩展窗口统计(最后10行):')
print(prices_ts[['收盘价', '累积均值', '累积标准差', '累积最大值']].tail(10))

# 年化累计波动率
prices_ts['累计波动率'] = prices_ts['收盘价'].pct_change().expanding().std() * np.sqrt(252)
print('\n年化累计波动率(最后5行):')
print(prices_ts['累计波动率'].tail())
扩展窗口统计(最后10行):
                   收盘价        累积均值      累积标准差       累积最大值
日期                                                       
2024-03-31  152.824201  124.387639  17.052224  156.940513
2024-04-01  155.036999  124.720784  17.256699  156.940513
2024-04-02  155.965186  125.056745  17.465786  156.940513
2024-04-03  153.973709  125.364372  17.625810  156.940513
2024-04-04  154.820070  125.674432  17.790369  156.940513
2024-04-05  156.090705  125.991269  17.966709  156.940513
2024-04-06  154.822990  126.288503  18.111043  156.940513
2024-04-07  155.630441  126.587910  18.259615  156.940513
2024-04-08  156.246858  126.887496  18.409149  156.940513
2024-04-09  154.460917  127.163230  18.522324  156.940513

年化累计波动率(最后5行):
日期
2024-04-05    0.251020
2024-04-06    0.250608
2024-04-07    0.249300
2024-04-08    0.248016
2024-04-09    0.248124
Name: 累计波动率, dtype: float64

⭐ rolling vs expanding 对比

特性 rolling expanding
窗口大小 固定(如5日、20日) 不断增长(从起点到当前)
计算范围 \([t-w+1, \; t]\) \([1, \; t]\)
典型应用 移动平均、短期波动 累计收益、长期风险

⭐ 分组统计 groupby:核心思想

分组聚合 (GroupBy Aggregation) 的三个步骤:

  1. Split(拆分):按键值将数据分成若干组
  2. Apply(应用):对每组数据应用聚合函数
  3. Combine(合并):将结果合并为一个新的数据结构

\[ \text{GroupBy}(K, f, X) = \{(k, f(\{x \mid key(x) = k\})) \mid k \in K\} \]

⭐ groupby 基础操作

Listing 14
industry_data = pd.DataFrame({
    '股票代码': ['600519.SH', '000858.SZ', '600036.SH', '601318.SH', '000001.SZ', '601398.SH'],
    '股票名称': ['贵州茅台', '五粮液', '招商银行', '中国平安', '平安银行', '工商银行'],
    '行业': ['白酒', '白酒', '银行', '保险', '银行', '银行'],
    '市盈率': [45.2, 35.8, 8.5, 12.3, 9.2, 6.8],
    '市净率': [12.3, 8.9, 0.9, 1.5, 1.1, 0.7],
    'ROE': [0.28, 0.22, 0.15, 0.18, 0.13, 0.14],
    '股息率': [0.012, 0.018, 0.035, 0.028, 0.040, 0.045]
})

# 按行业分组计算均值
industry_mean = industry_data.groupby('行业').mean(numeric_only=True)
print('行业均值:')
print(industry_mean)
行业均值:
          市盈率   市净率   ROE    股息率
行业                              
保险  12.300000   1.5  0.18  0.028
白酒  40.500000  10.6  0.25  0.015
银行   8.166667   0.9  0.14  0.040

⭐ groupby 多统计量聚合 (agg)

Listing 15
industry_stats = industry_data.groupby('行业').agg({
    '市盈率': ['mean', 'median', 'std'],
    '市净率': ['mean', 'min', 'max'],
    'ROE': 'mean',
    '股息率': 'mean'
})
print('行业详细统计:')
print(industry_stats.round(4))

# 分组计数
print('\n行业股票数量:')
print(industry_data.groupby('行业').size())
行业详细统计:
        市盈率                  市净率              ROE    股息率
       mean median     std  mean  min   max  mean   mean
行业                                                      
保险  12.3000   12.3     NaN   1.5  1.5   1.5  0.18  0.028
白酒  40.5000   40.5  6.6468  10.6  8.9  12.3  0.25  0.015
银行   8.1667    8.5  1.2342   0.9  0.7   1.1  0.14  0.040

行业股票数量:
行业
保险    1
白酒    2
银行    3
dtype: int64

⭐ 多级分组统计

Listing 16
multi_level_data = pd.DataFrame({
    '行业': ['白酒', '白酒', '白酒', '银行', '银行', '银行', '保险', '保险'],
    '市值等级': ['大盘', '中盘', '大盘', '大盘', '小盘', '大盘', '大盘', '中盘'],
    '市盈率': [45.2, 35.8, 38.5, 8.5, 15.2, 6.8, 12.3, 18.5],
    '收益率': [0.05, 0.03, 0.06, 0.02, 0.04, 0.01, 0.03, 0.02]
})

df_multi = pd.DataFrame(multi_level_data)

# 多级分组
multi_stats = df_multi.groupby(['行业', '市值等级']).agg({
    '市盈率': 'mean',
    '收益率': 'mean'
})
print('多级分组统计:')
print(multi_stats)
多级分组统计:
           市盈率    收益率
行业 市值等级              
保险 中盘    18.50  0.020
   大盘    12.30  0.030
白酒 中盘    35.80  0.030
   大盘    41.85  0.055
银行 大盘     7.65  0.015
   小盘    15.20  0.040

⭐ groupby 行业内排名

Listing 17
df_multi['行业内PE排名'] = df_multi.groupby('行业')['市盈率'].rank()
print('行业内PE排名:')
print(df_multi)
行业内PE排名:
   行业 市值等级   市盈率   收益率  行业内PE排名
0  白酒   大盘  45.2  0.05      3.0
1  白酒   中盘  35.8  0.03      1.0
2  白酒   大盘  38.5  0.06      2.0
3  银行   大盘   8.5  0.02      2.0
4  银行   小盘  15.2  0.04      3.0
5  银行   大盘   6.8  0.01      1.0
6  保险   大盘  12.3  0.03      1.0
7  保险   中盘  18.5  0.02      2.0

⭐ 自定义聚合函数

Listing 18
# 自定义:计算市净率/市盈率比率均值
def price_to_book_ratio(group):
    '''计算市净率/市盈率比率'''
    return (group['市净率'] / group['市盈率']).mean()

# 自定义:基于ROE计算类夏普比率
def roe_adjusted_ratio(group, benchmark_roe=0.10):
    '''计算ROE超额收益与波动的比率'''
    excess_roe = group['ROE'].mean() - benchmark_roe
    return excess_roe / group['ROE'].std() if group['ROE'].std() > 0 else 0

print('市净率/市盈率比率:')
print(industry_data.groupby('行业').apply(price_to_book_ratio))

print('\n市盈率范围(最大-最小):')
print(industry_data.groupby('行业')['市盈率'].agg(lambda x: x.max() - x.min()))
市净率/市盈率比率:
行业
保险    0.121951
白酒    0.260364
银行    0.109463
dtype: float64

市盈率范围(最大-最小):
行业
保险    0.0
白酒    9.4
银行    2.4
Name: 市盈率, dtype: float64

⭐ 协方差与相关系数

协方差 (Covariance):

\[ \text{Cov}(X, Y) = \frac{1}{n-1}\sum_{i=1}^n (x_i - \bar{x})(y_i - \bar{y}) \]

相关系数 (Correlation Coefficient):

\[ \rho_{X,Y} = \frac{\text{Cov}(X, Y)}{\sigma_X \sigma_Y} \]

  • \(\rho \approx 1\):强正相关(同涨同跌)
  • \(\rho \approx 0\):无相关(独立变动)
  • \(\rho \approx -1\):强负相关(此消彼长)

⭐ 相关性矩阵计算

Listing 19
# 使用之前的收益率数据
cov_matrix = df_returns.cov()
print('协方差矩阵:')
print(cov_matrix.round(6))

corr_matrix = df_returns.corr()
print('\n相关系数矩阵:')
print(corr_matrix.round(4))
协方差矩阵:
          贵州茅台       五粮液      招商银行      中国平安
贵州茅台  0.000330 -0.000059  0.000056 -0.000049
五粮液  -0.000059  0.000568 -0.000014 -0.000007
招商银行  0.000056 -0.000014  0.000265 -0.000000
中国平安 -0.000049 -0.000007 -0.000000  0.000253

相关系数矩阵:
        贵州茅台     五粮液    招商银行    中国平安
贵州茅台  1.0000 -0.1364  0.1908 -0.1702
五粮液  -0.1364  1.0000 -0.0366 -0.0176
招商银行  0.1908 -0.0366  1.0000 -0.0003
中国平安 -0.1702 -0.0176 -0.0003  1.0000

⭐ 找出相关性最高的股票对

Listing 20
corr_unstack = corr_matrix.unstack()
corr_unstack = corr_unstack[corr_unstack != 1]  # 排除自相关
top_corr = corr_unstack.abs().sort_values(ascending=False).head(5)
print('相关性最高的股票对:')
print(top_corr)
相关性最高的股票对:
贵州茅台  招商银行    0.190840
招商银行  贵州茅台    0.190840
贵州茅台  中国平安    0.170227
中国平安  贵州茅台    0.170227
贵州茅台  五粮液     0.136422
dtype: float64

⭐ 相关性在投资组合中的应用

现代投资组合理论 (MPT):

\[ \sigma_p^2 = \sum_{i=1}^n \sum_{j=1}^n w_i w_j \sigma_i \sigma_j \rho_{ij} \]

相关系数 投资含义 策略建议
\(\rho \approx 1\) 同涨同跌 分散化效果差
\(\rho \approx 0\) 独立变动 有效分散化
\(\rho \approx -1\) 此消彼长 天然对冲工具

\(\rho_{ij} < 1\) 时,组合方差小于各资产方差的加权平均,实现风险分散

⭐ 本章小结

模块 核心方法 典型应用
描述性统计 mean(), std(), describe() 收益率分析
分位数与异常值 quantile(), IQR 规则 风险监控
偏度与峰度 skew(), kurtosis() 分布特征判断
累积统计 cumsum(), cummax() 回撤分析
滚动窗口 rolling(), expanding() 移动平均、布林带
分组统计 groupby(), agg() 行业对比分析
相关性分析 corr(), cov() 投资组合优化